package com.leo.train.business.service.Impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.leo.train.business.dto.ConfirmOrderDto;
import com.leo.train.business.dto.ConfirmOrderSaveDto;
import com.leo.train.business.dto.ConfirmOrderTicketDto;
import com.leo.train.business.entity.ConfirmOrder;
import com.leo.train.business.entity.DailyCarriage;
import com.leo.train.business.entity.DailySeat;
import com.leo.train.business.entity.DailyTrainTicket;
import com.leo.train.business.mapper.ConfirmOrderMapper;
import com.leo.train.business.service.*;
import com.leo.train.common.enums.ConfirmOrderStatusEnum;
import com.leo.train.common.enums.RedisKeyPreEnum;
import com.leo.train.common.enums.SeatColEnum;
import com.leo.train.common.enums.SeatTypeEnum;
import com.leo.train.common.exception.BusinessException;
import com.leo.train.common.exception.BusinessExceptionEnum;
import com.leo.train.common.resp.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class ConfirmOrderServiceImpl implements ConfirmOrderService {
    @Autowired
    private ConfirmOrderMapper confirmOrderMapper;
    @Autowired
    private DailyTrainTicketService dailyTrainTicketService;
    @Autowired
    private DailyCarriageService dailyCarriageService;
    @Autowired
    private DailySeatService dailySeatService;
    @Autowired
    private AfterConfirmOrderService afterConfirmOrderService;
    @Autowired
    private SkTokenService skTokenService;
//    @Autowired
//    private RedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageResult<ConfirmOrder> list(ConfirmOrderDto confirmOrderDto) {
        PageHelper.startPage(confirmOrderDto.getPage(), confirmOrderDto.getPageSize());
        Page<ConfirmOrder> page = confirmOrderMapper.list(confirmOrderDto);
        return new PageResult<>(page.getTotal(), page.getResult());
    }

    /**
     * 确认订单
     * @param confirmOrder
     */
    @Override
    @SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock") // 配置限流
    @Async // 异步线程执行
    public void doConfirm(ConfirmOrder confirmOrder) {
        MDC.put("LOG_ID", confirmOrder.getLogId());
        log.info("异步出票开始：{}", confirmOrder);

        // 获取锁: 锁的粒度，越细越快，这里用日期+车次编号作为锁的key
        // 注意：这里的锁是为了防止多个用户同时购买同一张票，导致余票超卖
        // 这里的锁时间设置为5秒，是为了防止用户购买后，服务器异常，导致锁一直存在，导致其他用户无法购买
        String lockKey = RedisKeyPreEnum.CONFIRM_ORDER  + "-" + confirmOrder.getDate() + "-" + confirmOrder.getTrainCode();
        RLock lock = null;
        try{
            // 使用redisson，自带看门狗
            lock = redissonClient.getLock(lockKey);
            boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS);
            if (!tryLock) {
                return;
            } else {
                log.info("获取锁成功：{}", lockKey);
            }

            // 采用循环遍历购票订单，以分页的形式每5条获取，防止死循环，导致CPU占用率过高
            while(true) {
                PageHelper.startPage(1, 5);
                // 取确认订单表的记录，同日期车次，状态是I，分页处理，每次取N条,以及以ID为升序排序
                List<ConfirmOrder> list = confirmOrderMapper.listByInfo(
                        confirmOrder.getDate(),
                        confirmOrder.getTrainCode(),
                        ConfirmOrderStatusEnum.INIT.getCode()
                );
                if (CollUtil.isEmpty(list)) {
                    break;
                } else {
                    log.info("本次处理{}条订单", list.size());
                }
                // 循环：一条一条的卖
                // 一条一条的卖
                list.forEach(confirmOrder1 -> {
                    try {
                        sell(confirmOrder1);
                    } catch (BusinessException e) {
                        if (e.getE().equals(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR)) {
                            log.info("本订单余票不足，继续售卖下一个订单");
                            confirmOrder1.setStatus(ConfirmOrderStatusEnum.EMPTY.getCode());
                            updateStatus(confirmOrder1);
                        } else {
                            throw e;
                        }
                    }
                });
            }
        }catch (Exception e){
            log.error("购票异常", e);
        }finally {
            log.info("购票流程结束，释放锁!");
            // 下边的判断是为了防止在finally中释放锁时，锁已经被其他线程释放了
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

    }

    /**
     * 查询前面有几个人在排队
     * @param id 确认订单ID
     * @return
     */
    @Override
    public Integer queryLineCount(Long id) {
        ConfirmOrder confirmOrder = confirmOrderMapper.selectById(id);
        // 获取确认订单状态枚举
        ConfirmOrderStatusEnum statusEnum = EnumUtil.getBy(ConfirmOrderStatusEnum::getCode, confirmOrder.getStatus());
        int result = switch (statusEnum) {
            case PENDING -> 0; // 排队0
            case SUCCESS -> -1; // 成功
            case FAILURE -> -2; // 失败
            case EMPTY -> -3; // 无票
            case CANCEL -> -4; // 取消
            case INIT -> 999; // 需要查表得到实际排队数量
        };
        if (result == 999) {
            // 排在第几位，下面的写法：where a=1 and (b=1 or c=1) 等价于 where (a=1 and b=1) or (a=1 and c=1)
            // 查询INIT和PENDING状态的订单数，这些订单的创建时间早于当前订单的创建时间，就是在排队的订单
            return confirmOrderMapper.countQueueOrders(confirmOrder.getDate(), confirmOrder.getTrainCode(), confirmOrder.getCreateTime());
        } else {
            return result;
        }
    }

    /**
     * 取消排队，只有I状态才能取消排队，所以按状态更新
     * @param id
     * @return
     */
    @Override
    public Integer cancel(Long id) {
        return confirmOrderMapper.cancelOrder(id, ConfirmOrderStatusEnum.INIT.getCode(), ConfirmOrderStatusEnum.CANCEL.getCode());
    }

    /**
     * 售票
     * @param confirmOrder
     */
    public void sell(ConfirmOrder confirmOrder) {
        // 1.为了演示排队效果，每次出票增加200毫秒延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 构造confirmOrderSaveDto
        ConfirmOrderSaveDto confirmOrderSaveDto = new ConfirmOrderSaveDto();
        confirmOrderSaveDto.setMemberId(confirmOrder.getMemberId());
        confirmOrderSaveDto.setDate(confirmOrder.getDate());
        confirmOrderSaveDto.setTrainCode(confirmOrder.getTrainCode());
        confirmOrderSaveDto.setStart(confirmOrder.getStart());
        confirmOrderSaveDto.setEnd(confirmOrder.getEnd());
        confirmOrderSaveDto.setDailyTrainTicketId(confirmOrder.getDailyTrainTicketId());
        confirmOrderSaveDto.setTickets(JSON.parseArray(confirmOrder.getTickets(), ConfirmOrderTicketDto.class));
        confirmOrderSaveDto.setImageCode("");
        confirmOrderSaveDto.setImageCodeToken("");
        confirmOrderSaveDto.setLogId("");

        // 将订单设置成处理中，避免重复处理
        log.info("将确认订单更新成处理中，避免重复处理，confirm_order.id: {}", confirmOrder.getId());
        confirmOrder.setStatus(ConfirmOrderStatusEnum.PENDING.getCode());
        updateStatus(confirmOrder);


        // 2. 查询余票记录，并判断余票是否充足
        String trainCode = confirmOrder.getTrainCode();
        Date date = confirmOrder.getDate();
        String start = confirmOrder.getStart();
        String end = confirmOrder.getEnd();
        List<ConfirmOrderTicketDto> tickets = confirmOrderSaveDto.getTickets(); // 获取订单的乘客信息
        DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(trainCode, date, start, end); // 查询出唯一的 余票记录
        log.info("查询余票记录：{}", dailyTrainTicket);
        // 3. 扣减余票数量，并判断余票是否足够
        reduceTickets(confirmOrderSaveDto, dailyTrainTicket);

        // 4. 选座（最复杂）
        // 最终的选座结果
        List<DailySeat> finalSeatList = new ArrayList<>();
        /**
         * 计算相对第一个座位的偏移值
         * 比如选择的是C1,D2，则偏移值是：[0,5]
         * 比如选择的是A1,B1,C1，则偏移值是：[0,1,2]
         */
        ConfirmOrderTicketDto ticketDto0 = tickets.get(0); // 取出第一个乘客的选座信息
        if(StrUtil.isNotBlank(ticketDto0.getSeat())) {
            log.info("本次选票有选座");
            // 查看本次选座的枚举类型
            List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketDto0.getSeatTypeCode());
            log.info("本次选座的座位类型包含的列：{}", colEnumList);
            // 组成和前端选座一样的列表
            List<String> referSeatList = new ArrayList<>();
            for(int i = 1;i <=2;i++) {
                for(SeatColEnum colEnum : colEnumList) {
                    referSeatList.add(colEnum.getCode() + i);
                }
            }
            log.info("用于做参考的座位列表：{}", referSeatList);
            // 计算出相对偏移列表
            List<Integer> offsetList = new ArrayList<>();
            // 先计算出绝对偏移列表
            List<Integer> absoluteOffsetList = new ArrayList<>();
            for(ConfirmOrderTicketDto ticketDto :tickets) {
                int offset = referSeatList.indexOf(ticketDto.getSeat());
                absoluteOffsetList.add(offset);
            }
            log.info("绝对偏移列表：{}", absoluteOffsetList);
            // 计算相对偏移列表
            for(int offset : absoluteOffsetList) {
                offsetList.add(offset - absoluteOffsetList.get(0));
            }
            log.info("相对偏移列表：{}", offsetList);

            // 4.1 挑选座位 一个车厢一个车厢的获取座位数据
            getSeat(
                    finalSeatList,
                    date,
                    trainCode,
                    ticketDto0.getSeatTypeCode(),
                    ticketDto0.getSeat().split( "")[0], // A1 -> A
                    offsetList,
                    dailyTrainTicket.getStartIndex(),
                    dailyTrainTicket.getEndIndex()
            );
        } else {
            log.info("本次选座无选座");
            for (ConfirmOrderTicketDto ticketDto : tickets) {
                // 4.1 挑选座位 一个车厢一个车厢的获取座位数据
                getSeat(
                        finalSeatList,
                        date,
                        trainCode,
                        ticketDto.getSeatTypeCode(),
                        null,
                        null,
                        dailyTrainTicket.getStartIndex(),
                        dailyTrainTicket.getEndIndex()
                );
            }
        }


        log.info("最终选座：{}", finalSeatList);

        // 5. 选中座位后，做事务处理
        // 5.1 座位表修改售卖情况sell字段
        // 5.2 余票详情表修改余票
        // 5.3 为会员增加购票记录（新增购票记录表）
        // 5.4 确认订单表修改状态为成功
        try {
            afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder);
        } catch (Exception e) {
            log.error("保存购票信息失败", e);
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
        }

    }

    /**
     * 更新状态
     * @param confirmOrder
     */
    public void updateStatus(ConfirmOrder confirmOrder) {
        ConfirmOrder confirmOrderForUpdate = new ConfirmOrder();
        confirmOrderForUpdate.setId(confirmOrder.getId());
        confirmOrderForUpdate.setUpdateTime(new Date());
        confirmOrderForUpdate.setStatus(confirmOrder.getStatus());
        confirmOrderMapper.updateByPrimaryKeySelective(confirmOrderForUpdate);
    }

    // 查询余票记录，并判断余票是否充足
    public void reduceTickets(ConfirmOrderSaveDto confirmOrderSaveDto, DailyTrainTicket dailyTrainTicket) {
        for (ConfirmOrderTicketDto ticketDto : confirmOrderSaveDto.getTickets()) {
            String seatTypeCode = ticketDto.getSeatTypeCode(); // 座位类型code
            // 根据座位类型code查询出对应的座位类型枚举
            SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode , seatTypeCode);
            switch(seatTypeEnum) {
                case YDZ -> {
                    int countLeft = dailyTrainTicket.getYdz() - 1;
                    if (countLeft < 0) {
                        // 余票不足
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setYdz(countLeft);
                }
                case EDZ -> {
                    int countLeft = dailyTrainTicket.getEdz() - 1;
                    if (countLeft < 0) {
                        // 余票不足
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setEdz(countLeft);
                }
                case RW -> {
                    int countLeft = dailyTrainTicket.getRw() - 1;
                    if (countLeft < 0) {
                        // 余票不足
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setRw(countLeft);
                }
                case YW -> {
                    int countLeft = dailyTrainTicket.getYw() - 1;
                    if (countLeft < 0) {
                        // 余票不足
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setYw(countLeft);
                }
            }

        }
    }

    // 挑选座位
    private void getSeat(List<DailySeat> finalSeatList,Date date, String trainCode, String seatType, String column, List<Integer> offsetList, Integer startIndex, Integer endIndex) {
        List<DailySeat> getSeatList = new ArrayList<>(); // 换车厢时需要清空
        List<DailyCarriage> carriageList = dailyCarriageService.listBySeatType(trainCode, date, seatType);
        log.info("共查出{}个符合条件的车厢", carriageList.size());
        // 4.1 一个车厢一个车厢的获取座位数据
        for (DailyCarriage dailyCarriage : carriageList) {
            log.info("开始从车厢{}选座", dailyCarriage.getIndex());
            List<DailySeat> seatList = dailySeatService.listByCarriage(date, trainCode, dailyCarriage.getIndex());
            log.info("车厢{}的座位数：{}", dailyCarriage.getIndex(), seatList.size());
            for (int i = 0; i < seatList.size(); i++) {
                DailySeat dailyTrainSeat = seatList.get(i);
                Integer seatIndex = dailyTrainSeat.getCarriageSeatIndex();
                String col = dailyTrainSeat.getCol();

                // 判断当前座位不能被选中过
                boolean alreadyChooseFlag = false;
                for (DailySeat finalSeat : finalSeatList){
                    if (finalSeat.getId().equals(dailyTrainSeat.getId())) {
                        alreadyChooseFlag = true;
                        break;
                    }
                }
                if (alreadyChooseFlag) {
                    log.info("座位{}被选中过，不能重复选中，继续判断下一个座位", seatIndex);
                    continue;
                }

                // 判断column，有值的话要比对列号
                if (StrUtil.isBlank(column)) {
                    log.info("无选座");
                } else {
                    if (!column.equals(col)) {
                        log.info("座位{}列值不对，继续判断下一个座位，当前列值：{}，目标列值：{}", seatIndex, col, column);
                        continue;
                    }
                }

                boolean isChoose = calSell(dailyTrainSeat, startIndex, endIndex);
                if (isChoose) {
                    log.info("选中座位");
                    getSeatList.add(dailyTrainSeat);
                } else {
                    continue;
                }

                // 根据offset选剩下的座位
                boolean isGetAllOffsetSeat = true;
                if (CollUtil.isNotEmpty(offsetList)) {
                    log.info("有偏移值：{}，校验偏移的座位是否可选", offsetList);
                    // 从索引1开始，索引0就是当前已选中的票
                    for (int j = 1; j < offsetList.size(); j++) {
                        Integer offset = offsetList.get(j);
                        // 座位在库的索引是从1开始
                        // int nextIndex = seatIndex + offset - 1;
                        int nextIndex = i + offset;

                        // 有选座时，一定是在同一个车箱
                        if (nextIndex >= seatList.size()) {
                            log.info("座位{}不可选，偏移后的索引超出了这个车箱的座位数", nextIndex);
                            isGetAllOffsetSeat = false;
                            break;
                        }

                        DailySeat nextDailyTrainSeat = seatList.get(nextIndex);
                        boolean isChooseNext = calSell(nextDailyTrainSeat, startIndex, endIndex);
                        if (isChooseNext) {
                            log.info("座位{}被选中", nextDailyTrainSeat.getCarriageSeatIndex());
                            getSeatList.add(nextDailyTrainSeat);
                        } else {
                            log.info("座位{}不可选", nextDailyTrainSeat.getCarriageSeatIndex());
                            isGetAllOffsetSeat = false;
                            break;
                        }
                    }
                }
                if (!isGetAllOffsetSeat) {
                    getSeatList = new ArrayList<>();
                    continue;
                }

                // 保存选好的座位
                finalSeatList.addAll(getSeatList);
                return;
            }
        }
    }

    /**
     * 计算某座位在区间内是否可卖
     * 例：sell=10001，本次购买区间站1~4，则区间已售000
     * 全部是0，表示这个区间可买；只要有1，就表示区间内已售过票
     *
     * 选中后，要计算购票后的sell，比如原来是10001，本次购买区间站1~4
     * 方案：构造本次购票造成的售卖信息01110，和原sell 10001按位与，最终得到11111
     */
    private boolean calSell(DailySeat dailyTrainSeat, Integer startIndex, Integer endIndex) {
        // 00001, 00000
        String sell = dailyTrainSeat.getSell();
        //  000, 000
        String sellPart = sell.substring(startIndex, endIndex);
        if (Integer.parseInt(sellPart) > 0) {
            log.info("座位{}在本次车站区间{}~{}已售过票，不可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex);
            return false;
        } else {
            log.info("座位{}在本次车站区间{}~{}未售过票，可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex);
            //  111,   111
            String curSell = sellPart.replace('0', '1');
            // 0111,  0111
            curSell = StrUtil.fillBefore(curSell, '0', endIndex);
            // 01110, 01110
            curSell = StrUtil.fillAfter(curSell, '0', sell.length());

            // 当前区间售票信息curSell 01110与库里的已售信息sell 00001按位与，即可得到该座位卖出此票后的售票详情
            // 15(01111), 14(01110 = 01110|00000)
            int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell);
            //  1111,  1110
            String newSell = NumberUtil.getBinaryStr(newSellInt);
            // 01111, 01110
            newSell = StrUtil.fillBefore(newSell, '0', sell.length());
            log.info("座位{}被选中，原售票信息：{}，车站区间：{}~{}，即：{}，最终售票信息：{}"
                    , dailyTrainSeat.getCarriageSeatIndex(), sell, startIndex, endIndex, curSell, newSell);
            dailyTrainSeat.setSell(newSell);
            return true;

        }
    }

    /**
     * 降级方法，需包含限流方法的所有参数和BlockException参数
     * @param confirmOrderSaveDto
     * @param blockException
     */
    private void doConfirmBlock(ConfirmOrderSaveDto confirmOrderSaveDto, BlockException blockException) {
        log.info("确认订单被限流或熔断，confirmOrderSaveDto={}", JSON.toJSONString(confirmOrderSaveDto));
        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_FLOW_EXCEPTION);
    }


}
